// Copyright (c) 2019 Presto Labs Pte. Ltd.
// Author: jaewon

#pragma once

#include <sstream>
#include <string>
#include <utility>

#include <boost/preprocessor/stringize.hpp>
#include <glog/logging.h>

// Utility macros for showing informative exception messages including stacktrace.

namespace detail {

std::string Stacktrace(bool strip_after_caller) noexcept __attribute__((noinline));

class ExceptionMessage {
 public:
  ExceptionMessage(const char* file, int line, std::string&& stacktrace_msg);
  ~ExceptionMessage() noexcept {}
  std::ostream& stream() { return stream_; }
  std::string str();

 private:
  std::string stacktrace_msg_;
  std::stringstream stream_;
};

template <typename T>
class ExceptionThrower {
 public:
  ExceptionThrower(const char* file, int line, std::string&& stacktrace_msg)
      : msg_(file, line, std::move(stacktrace_msg)) {}

  ~ExceptionThrower() noexcept(false) {
    // Throwing an exception is regarded as bad practice. Howevenr, this class will only be
    // uses as nameless temporaries, considered to be the only allowed case.
    throw T(msg_.str());
  }

  std::ostream& stream() { return msg_.stream(); }

 private:
  ExceptionMessage msg_;
};

}  // namespace detail

#define THROW(exception) \
  ::detail::ExceptionThrower<exception>(__FILE__, __LINE__, ::detail::Stacktrace(true)).stream()
#define THROW_INVALID_ARGUMENT() THROW(std::invalid_argument)
#define THROW_OUT_OF_RANGE() THROW(std::out_of_range)
#define THROW_RUNTIME_ERROR() THROW(std::runtime_error)

#define CHECK_THROW_INTERNAL(condition, exception) \
  if (__builtin_expect(!(condition), 0)) THROW(exception) << "Check failed: "

#if defined(CHECK_THROW)
// Defined at folly exception but nobody will use it.
// https://github.com/facebook/folly/blob/master/folly/Exception.h
#undef CHECK_THROW
#endif
#define CHECK_THROW(condition, exception) \
  CHECK_THROW_INTERNAL(condition, exception) BOOST_PP_STRINGIZE(condition) " "

#define CHECK_OP_THROW(op, val1, val2, exception)                                                  \
  CHECK_THROW_INTERNAL((val1)op(val2), exception)                                                  \
  BOOST_PP_STRINGIZE(val1)                                                                         \
      " " BOOST_PP_STRINGIZE(op) " " BOOST_PP_STRINGIZE(val2) " ("                                 \
      << ::google::GetReferenceableValue(val1) << " vs. " << ::google::GetReferenceableValue(val2) \
      << ") "

#define CHECK_EQ_THROW(val1, val2, exception) CHECK_OP_THROW(==, val1, val2, exception)
#define CHECK_NE_THROW(val1, val2, exception) CHECK_OP_THROW(!=, val1, val2, exception)
#define CHECK_LE_THROW(val1, val2, exception) CHECK_OP_THROW(<=, val1, val2, exception)
#define CHECK_LT_THROW(val1, val2, exception) CHECK_OP_THROW(<, val1, val2, exception)
#define CHECK_GE_THROW(val1, val2, exception) CHECK_OP_THROW(>=, val1, val2, exception)
#define CHECK_GT_THROW(val1, val2, exception) CHECK_OP_THROW(>, val1, val2, exception)

#if DCHECK_IS_ON()

#define DCHECK_THROW(condition, exception) CHECK_THROW(condition, exception)
#define DCHECK_EQ_THROW(val1, val2, exception) CHECK_EQ_THROW(val1, val2, exception)
#define DCHECK_NE_THROW(val1, val2, exception) CHECK_NE_THROW(val1, val2, exception)
#define DCHECK_LE_THROW(val1, val2, exception) CHECK_LE_THROW(val1, val2, exception)
#define DCHECK_LT_THROW(val1, val2, exception) CHECK_LT_THROW(val1, val2, exception)
#define DCHECK_GE_THROW(val1, val2, exception) CHECK_GE_THROW(val1, val2, exception)
#define DCHECK_GT_THROW(val1, val2, exception) CHECK_GT_THROW(val1, val2, exception)

#else  // DCHECK_IS_ON()

#define DCHECK_THROW(condition, exception) \
  while (false) CHECK_THROW(condition, exception)
#define DCHECK_EQ_THROW(val1, val2, exception) \
  while (false) CHECK_EQ_THROW(val1, val2, exception)
#define DCHECK_NE_THROW(val1, val2, exception) \
  while (false) CHECK_NE_THROW(val1, val2, exception)
#define DCHECK_LE_THROW(val1, val2, exception) \
  while (false) CHECK_LE_THROW(val1, val2, exception)
#define DCHECK_LT_THROW(val1, val2, exception) \
  while (false) CHECK_LT_THROW(val1, val2, exception)
#define DCHECK_GE_THROW(val1, val2, exception) \
  while (false) CHECK_GE_THROW(val1, val2, exception)
#define DCHECK_GT_THROW(val1, val2, exception) \
  while (false) CHECK_GT_THROW(val1, val2, exception)

#endif  // DCHECK_IS_ON()
